package com.me.permission;

import com.me.permission.annotation.PermissionColumns;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.mybatis.logging.Logger;
import org.mybatis.logging.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Mybatis 插件：数据权限
 *
 * @author ME
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class PermissionInterceptor implements Interceptor {

    private final static Logger logger = LoggerFactory.getLogger(PermissionInterceptor.class);

    private ConcurrentHashMap<PermissionObj, String> permissionSqlCache = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            PermissionObj localPermis = PermissionHelper.getLocalPermission();
            if (localPermis == null || Objects.isNull(localPermis.getPermissionSet()) || CollectionUtils.isEmpty(localPermis.getPermissionSet())) {
                return invocation.proceed();
            }
            Object[] args = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler<?> resultHandler = (ResultHandler<?>) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系，只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = mappedStatement.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }

            // 获取到SQL ，进行调整
            String newSql = processSelectSql(boundSql, localPermis);
            logger.debug(() -> "data permission sql: " + newSql);
            BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql, boundSql.getParameterMappings(), parameter);

            // 执行 带数据权限的查询
            return executor.query(mappedStatement, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
        } finally {
            PermissionHelper.clearPermission();
        }
    }

    /**
     * 解析 mybatis配置的sql，并动态添加 数据权限筛选条件
     *
     * @param boundSql    mybatis 中的 BoundSql对象
     * @param localPermis 数据权限本地变量
     * @return
     * @throws JSQLParserException
     */
    private String processSelectSql(BoundSql boundSql, PermissionObj localPermis) throws JSQLParserException {
        String sql = boundSql.getSql();
        Select select = (Select) CCJSqlParserUtil.parse(sql);
        PlainSelect selectBody = (PlainSelect) select.getSelectBody();

        Expression where = selectBody.getWhere();
        Table table = getPermissionTable(selectBody, localPermis);
        if (table == null) {
            throw new RuntimeException("数据权限：没有找到需要校验数据权限的表~~");
        }

        // 拼接 数据权限相关的条件语句
        String dataPermisSql = getPermisSql(table, localPermis);
        if (where == null) {
            selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(dataPermisSql));
        } else {
            selectBody.setWhere(new AndExpression(where, CCJSqlParserUtil.parseCondExpression(dataPermisSql)));
        }
        return selectBody.toString();
    }

    /**
     * 生成 筛选数据权限的 where条件语句
     *
     * @param table       sql解析对象
     * @param localPermis 数据权限本地变量
     * @return
     */
    private String getPermisSql(Table table, PermissionObj localPermis) {
        // 数据权限的条件语句模板
        String permisSql = permissionSqlCache.get(localPermis);
        // 把权限列表拼接成 `,`分隔的字符串
        StringJoiner joiner = new StringJoiner(",");
        localPermis.getPermissionSet().forEach(joiner::add);
        String permisParam = joiner.toString();
        if (permisSql != null) {
            return String.format(permisSql, permisParam);
        }

        Class<?> conditionClazz = localPermis.getConditionClazz();
        PermissionColumns permissionColumns = conditionClazz.getDeclaredAnnotation(PermissionColumns.class);
        if (permissionColumns == null) {
            throw new RuntimeException("需要控制数据权限的指定类未定义 @PermissionColumns：" + conditionClazz.getName());
        }
        String[] columnNames = permissionColumns.value();
        int index = localPermis.getPermissionColumnIndex();
        if (index > columnNames.length - 1) {
            throw new RuntimeException("permissionColumnIndex 下标越界，请检查注解配置的 @PermissionColumns");
        }

        StringBuilder permisStrBuilder = new StringBuilder();
        permisStrBuilder.append(table.getAlias()).append(".").append(columnNames[index])
                .append(" in (%s)");
        permissionSqlCache.put(localPermis, permisStrBuilder.toString());
        return String.format(permisStrBuilder.toString(), permisParam);
    }

    /**
     * 获取 sql 中用来执行数据权限的筛选的 表
     *
     * @param selectBody  sql解析对象
     * @param localPermis 数据权限本地变量
     * @return
     */
    private Table getPermissionTable(PlainSelect selectBody, PermissionObj localPermis) {
        Table table;
        FromItem fromItem = selectBody.getFromItem();

        if (fromItem instanceof Table) {
            table = (Table) selectBody.getFromItem();
        } else {
            throw new RuntimeException("数据权限：sql解析失败");
        }
        if (checkConditionTableName(table, localPermis)) {
            return table;
        }

        List<Join> joins = selectBody.getJoins();
        if (joins != null) {
            for (Join join : joins) {
                table = ((Table) join.getRightItem());
                if (checkConditionTableName(table, localPermis)) {
                    return table;
                }
            }
        }
        return table;
    }

    /**
     * 校验是否和 用来做数据权限筛选的 表名相同
     *
     * @param table       sql解析出来的表对象
     * @param localPermis 数据权限本地变量
     * @return
     */
    private boolean checkConditionTableName(Table table, PermissionObj localPermis) {
        String tableName = table.getName();
        if (tableName.startsWith("`")) {
            // 如果表名是 `t_account_wide_qt_bus_unit` 形式
            tableName = tableName.replace("`", "");
        }
        return tableName.equals(localPermis.getConditionClazz().getDeclaredAnnotation(PermissionColumns .class).tableName());
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

}
